/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package br.ufrj.dcc.pushpeer.protocol;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import br.ufrj.dcc.pushpeer.dht.MD5;
import br.ufrj.dcc.pushpeer.dht.SimpleDHT;
import br.ufrj.dcc.pushpeer.events.Event;
import br.ufrj.dcc.pushpeer.events.EventType;
import br.ufrj.dcc.pushpeer.events.IntegerEvent;

import peersim.cdsim.CDProtocol;
import peersim.core.*;
import peersim.config.*;
import peersim.edsim.*;
import peersim.transport.*;

/**
 * This is the class that implements the PushPeer module for PeerSim.
 */
public abstract class PushPeer implements EDProtocol, CDProtocol{

	/*******************************************************/
	/**-------------------- CONSTANTS --------------------**/
	/*******************************************************/
	
	/**
	 * Enables the replication strategy.
	 */
	public static final boolean ENABLE_CACHE = true;
	
	/**
	 * A peer is a leecher when it's still downloading a file. 
	 */
	public static final int LEECHER = 1;
	
	/**
	 * A peer is a seeder when it has already finished downloading a file. 
	 */
	public static final int SEEDER = 2;
	
	/**
	 * A peer is not interested when it doesn't wish to download a file. 
	 */
	public static final int NOT_INTERESTED = 3;
	
	/**
	 * Means a file is wanted (to be downloaded).
	 */
	public static final boolean WANTED = true;
	
	/**
	 * The size of a piece in Kb.
	 */
	public static final int SIZE_PIECE = 2048;
	
	/**
	 * The number of blocks that make up a piece of a file.
	 */
	public static final int NUM_BLOCKS = 16;
	
	/**
	* The transport protocol to use.
	*/
	protected static final String PAR_TRANSPORT = "transport";
	
	/**
	 * The size in Megabytes of the files being shared.
	 */
	protected static final String FILE_SIZE = "file_size";
	
	/**
	 * Maximum size the swarm is allowed to grow.
	 */
	protected static final String SWARM_MAX_SIZE = "swarm_max_size";

	/**
	 * Minimum size the swarm is allowed to shrink.
	 */
	protected static final String SWARM_MIN_SIZE = "swarm_min_size";
	
	/**
	 * Maximum size the set of known peers is allowed to grow.
	 */
	protected static final String KNOWN_PEERS_MAX_SIZE = "known_peers_max_size";
	
	/**
	 * Minimum size the set of known peers is allowed to shrink.
	 */
	protected static final String KNOWN_PEERS_MIN_SIZE = "known_peers_min_size";
	
	/**
	 * Rate at which the fetch size grows if the last fetch is useless.
	 */
	protected static final int SWARM_FETCH_GROWTH_RATIO = 2;
	
	/*******************************************************/
	/**------------------- CONSTRUCTOR -------------------**/
	/*******************************************************/
	
	/**
	 * The default constructor. Reads the configuration file and initializes the
	 * configuration parameters.
	 * @param prefix the component prefix declared in the configuration file.
	 */
	public PushPeer(String prefix) {
		
		tid = Configuration.getPid(prefix+"."+PAR_TRANSPORT);
		
		fileNumPiecesCompleted = new HashMap<Integer, Integer>();
		blocksStatus = new HashMap<Integer, List<Integer>>();
		peerStatus = new HashMap<Integer, Integer>();
		wantedFiles = new HashMap<Integer, Boolean>();
		
		nPieces = (int)((Configuration.getInt(prefix + "." + FILE_SIZE))*8*1024/SIZE_PIECE);
		swarm_max = (int)(Configuration.getInt(prefix + "." + SWARM_MAX_SIZE));
		swarm_min = (int)(Configuration.getInt(prefix + "." + SWARM_MIN_SIZE));
		known_max = (int)(Configuration.getInt(prefix + "." + KNOWN_PEERS_MAX_SIZE));
		known_min = (int)(Configuration.getInt(prefix + "." + KNOWN_PEERS_MIN_SIZE));
		
		managedPeers = new HashSet<Node>();
		managedPeerCacheCount = new HashMap<Node, Integer>();
		swarm = new HashMap<Integer, Set<Node>>();
		knownPeers = new HashSet<Node>();
		knownPeersQueue = new LinkedList<Node>();
		
		alreadyPolled =  new HashMap<Integer, Boolean>();
		downloadCompletionTime = new HashMap<Integer, Long>();
	}
	
	/*******************************************************/
	/**-------------------- VARIABLES --------------------**/
	/*******************************************************/
	
	/**
	 * The ID of the current node.
	 */
	protected long thisNodeID;
	
	/**
	 * The maximum connection speed of the local node.
	 */
	protected int maxBandwidth;
	
	/**	
	 * Length of the file. Stored as number of pieces (256KB each one).
	 */
	protected int nPieces;
	
	/**
	* Transport identifier, obtained from config property.
	*/
	protected final int tid;
	
	/**
	 * Maximum size the swarm is allowed to grow.
	 */
	protected int swarm_max;
	
	/**
	 * Minimum size the swarm is allowed to shrink.
	 */
	protected int swarm_min;
	
	/**
	 * Maximum size the set of known peers is allowed to grow.
	 */
	protected int known_max;
	
	/**
	 * Minimum size the set of known peers is allowed to shrink.
	 */
	protected int known_min;
	
	/**
	 *	Maps a file to the number of it's completed pieces.
	 */
	protected Map<Integer, Integer> fileNumPiecesCompleted;
	
	/**
	 *	Maps a file to a list which has the number of completed blocks for each piece.
	 */
	protected Map<Integer, List<Integer>> blocksStatus;
	
	/**
	 *	Maps a file to it's status ({@link #LEECHER}, {@link #SEEDER} or {@link #NOT_INTERESTED}).
	 */
	protected Map<Integer, Integer> peerStatus;
	
	/**
	 * Holds the currently wanted (to be downloaded) files. {@link #WANTED} means the file is wanted. 
	 */
	protected Map<Integer, Boolean> wantedFiles;
	
	/**
	 * Holds the known peers that have at least one piece of a file.
	 */
	protected Map<Integer, Set<Node>> swarm;
	
	/**
	 * Holds the currently to-be-cached file. Negative values means no file is currently to-be-cached.
	 */
	protected int toBeCachedFile = -1;
	
	/**
	 * The previous peer state of the currently cached file.
	 */
	protected int cachedFilePreviousPeerState;
	
	/**
	 * This peer's Cache Bank (CB).
	 */
	protected Node cb;
	
	/**
	 * Keeps a reasonably sized Set of peers this peer has interacted with in the past.
	 */
	protected Set<Node> knownPeers;
	
	/**
	 * Auxiliary structure to implement a FIFO policy.
	 */
	protected Queue<Node> knownPeersQueue;
	
	/**
	 * This peer's managed peers.
	 */
	protected Set<Node> managedPeers;
	
	/**
	 * Holds the amount of completed cache cycles each managed peer has respected.
	 */
	protected Map<Node, Integer> managedPeerCacheCount;
	
	/**
	 * Holds the remaining upload time until this peer goes back to idle. 
	 */
	protected int remainingUploadsTime = 0;
	
	/*
	 * The time this peer has been created.
	 */
	protected long initialTime;
	
	/*
	 * Whether or not this peer has been polled by the DownloadCompletionObserver already for download completion times. 
	 */
	public Map<Integer, Boolean> alreadyPolled;
	
	/*
	 * Holds the download completion time for each file.
	 */
	public Map<Integer, Long> downloadCompletionTime;
	
	/*******************************************************/
	/**--------------------- METHODS ---------------------**/
	/*******************************************************/
	
	/**
	 * Gets the maximum connection speed of the local node.
	 * @return The maximum connection speed of the local node.
	 */
	public int getMaxBandwidth() {
		return maxBandwidth;
	}

	/**
	 * Sets the maximum connection speed of the local node.
	 * @param maxBandwidth The maximum connection speed of the local node.
	 */
	public void setMaxBandwidth(int maxBandwidth) {
		this.maxBandwidth = maxBandwidth;
	}
	
	/**
	 * Gets the ID of the current node.
	 * @return The ID of the current node.
	 */
	public long getThisNodeID() {
		return thisNodeID;
	}

	/**
	 * Sets the ID of the current node.
	 * @param thisNodeID The ID of the current node.
	 */
	public void setThisNodeID(long thisNodeID) {
		this.thisNodeID = thisNodeID;
	}

	/**
	 * Returns the file's size as number of pieces of 256KB.
	 * @return number of pieces that compose the file.
	 */
	public int getNPieces(){
		return nPieces;	
	}
	
	/**
	 * Increases the counter with the number of pieces already completed for a given file.
	 * @param file The file to set the number of completed pieces.
	 */
	public void increaseFileNumPiecesCompleted(int file) {
		fileNumPiecesCompleted.put(file, fileNumPiecesCompleted.get(file) + 1);
	}
	
	/**
	 * Sets the counter with the number of pieces already completed for a given file.
	 * @param file The file to set the number of completed pieces.
	 * @param num The number of completed pieces.
	 */
	public void setFileNumPiecesCompleted(int file, int num) {
		fileNumPiecesCompleted.put(file, num);
	}
	
	/**
	 * Returns the number of pieces already completed for a given file.
	 * @param file The file to get the number of completed pieces.
	 * @return The number of completed pieces of the given file.
	 */
	public int getFileNumPiecesCompleted(int file) {
		if(fileNumPiecesCompleted.get(file) == null)
			fileNumPiecesCompleted.put(file, 0);
		return fileNumPiecesCompleted.get(file);
	}
	
	/**
	 * Returns the number of completed blocks for a piece of a given file.
	 * @param file The file.
	 * @param piece The piece of the given file.
	 * @return The number of completed blocks on the piece of a given file. Returns 0 if the file is not initialized yet.
	 */
	public int getFilePieceBlocksStatus(int file, int piece) {
		List<Integer> blocks = blocksStatus.get(file);
		if(blocks == null)
			return 0;
		else
			return blocks.get(piece);
	}

	/**
	 * Sets the number of completed blocks for a piece of a given file.
	 * @param file The file.
	 * @param piece The piece of the given file.
	 * @param num_blocks The number of completed blocks on the piece of the given file.
	 */
	public void setFilePieceBlocksStatus(int file, int piece, int num_blocks) {
		if(blocksStatus.get(file) == null) {
			List<Integer> blocks;
			blocks = new ArrayList<Integer>();
			for(int i=0; i<nPieces; i++) {
				blocks.add(0);
			}
			blocksStatus.put(file, blocks);
		}
		blocksStatus.get(file).set(piece, num_blocks);
		if(blocksStatus.get(file).get(piece) == NUM_BLOCKS) {
			increaseFileNumPiecesCompleted(file);
		}
	}

	/**
	 * Sets the currently wanted status for a file.
	 * @param file The file.
	 * @param status The wanted status for this file. It can be {@link #WANTED} or not.
	 */
	public void setFileWantedStatus(int file, boolean status) {
		wantedFiles.put(file, status);
	}
	
	/**
	 * Gets the currently wanted status for a file.
	 * @param file The file.
	 * @return {@link #WANTED} if the file is currently wanted.
	 */
	public boolean getFileWantedStatus(int file) {
		if(!wantedFiles.containsKey(file))
			wantedFiles.put(file, false);
		return wantedFiles.get(file);
	}
	
	/**
	 * Sets a file to be cached.
	 * @param toBeCachedFile The file to be cached.
	 */
	public void setToBeCachedFile(Node node, int pid, int toBeCachedFile) {
		
		//Same file asked previously, do nothing.
		if(this.toBeCachedFile == toBeCachedFile)
			return;
		
		//Peer was not interested in the file before, need to delete it.
		if(this.toBeCachedFile != -1) {
			if(cachedFilePreviousPeerState == NOT_INTERESTED) {
				setFileWantedStatus(this.toBeCachedFile, !WANTED);
				setPeerStatus(this.toBeCachedFile, NOT_INTERESTED);
				//Delete the file because it's not wanted anymore.
				for(int i=0; i<nPieces; i++)
					setFilePieceBlocksStatus(this.toBeCachedFile, i, 0);
				setFileNumPiecesCompleted(this.toBeCachedFile, 0);
				
				downloadCompletionTime.put(this.toBeCachedFile, Long.MIN_VALUE);
				alreadyPolled.put(this.toBeCachedFile, false);
			}
		}
		
		this.toBeCachedFile = toBeCachedFile;
		cachedFilePreviousPeerState = getPeerStatus(toBeCachedFile);
		
		//Already seeding the file, do nothing.
		if(getPeerStatus(toBeCachedFile) == SEEDER) {
			return;
		}
		
		//If the file wasn't wanted already, it has to be initialized properly.
		setFileWantedStatus(toBeCachedFile, WANTED);
		setPeerStatus(toBeCachedFile, LEECHER);
		if(getFileNumPiecesCompleted(toBeCachedFile) == 0)
			for(int i=0; i<nPieces; i++)
				setFilePieceBlocksStatus(toBeCachedFile, i, 0);
		nextRequest(node, pid, toBeCachedFile);
	}
	
	/**
	 * Gets the file to be cached.
	 * @return The file to be cached.
	 */
	public int getToBeCachedFile() {
		return toBeCachedFile;
	}
	
	/**
	 * Sets the status of the local node for a given file.
	 * @param file The file to set the status.
	 * @param status The status of the node. Accepts {@link #LEECHER}, {@link #SEEDER} or {@link #NOT_INTERESTED}.
	 */
	public void setPeerStatus(int file, int status) {
		peerStatus.put(file, status);
	}
	
	/**
	 * Returns the status of the local node for a given file.
	 * @param file The file.
	 * @return The status of this file. Either {@link #LEECHER}, {@link #SEEDER} or {@link #NOT_INTERESTED}.
	 */
	public int getPeerStatus(int file) {
		return peerStatus.get(file);
	}
	
	/**
	 * Sets this peer's Cache Bank Set (CBS).
	 * @param cbs This peer's Cache Bank Set.
	 */
	public void setCB(Node cb) {
		this.cb = cb;
	}
	
	/**
	 * Returns this peer's Cache Bank Set (CBS).
	 * @return This peer's Cache Bank Set.
	 */
	public Node getCB() {
		return this.cb;
	}

	/**
	 * Adds a peer to this peer's set of known peers. Should be called by every internal method that fetches new peers from the DHT.
	 * @param knownPeer Peer to be added to the set of known peers.
	 */
	protected void addKnownPeer(Node knownPeer) {
		if(knownPeers.size() > known_max) {
			Node removed = knownPeersQueue.poll();
			knownPeers.remove(removed);
		}
		
		knownPeers.add(knownPeer);
		knownPeersQueue.add(knownPeer);
	}

	/**
	 * Fetches a set of peers this peer has recently interacted with.
	 * @return A set of peers.
	 */
	public Set<Node> getKnownPeers() {
		//Populates the knownPeers with some pseudo-random peers via DHT, if there are too few of them.
		//Should only happen at the very beginning of the simulation.
		if(knownPeers.size() < known_min)
			for(Node n : SimpleDHT.findNodesByID(thisNodeID, (short)(10 - knownPeers.size())))
				addKnownPeer(n);
		
		return knownPeers;
	}
	
	/**
	 * Adds a managed peer to this peer.
	 * @param managedPeers The managed peer.
	 */
	public void setManagedPeer(Node managedPeer) {
		managedPeers.add(managedPeer);
		managedPeerCacheCount.put(managedPeer, 0);
		addKnownPeer(managedPeer);
	}
	
	/**
	 * Removes a managed peer from this peer.
	 * @param managedPeers The managed peer to be removed.
	 */
	public void unsetManagedPeer(Node managedPeer) {
		managedPeers.remove(managedPeer);
	}
	
	/**
	 * Returns this peer's managed peers Set.
	 * @return This peer's managed peers Set.
	 */
	public Set<Node> getManagedPeers() {
		return managedPeers;
	}

	/**
	 * Gets the rarest (least completed) files, choosing only from the files this peer is a leecher of.
	 * @param size Number of elements to be returned.
	 * @return A descending list with the rarest files.
	 */
	public List<Integer> getRarestFile(int size) {
		//<Number of pieces, List<File number>>. I know it's not very intuitive, but I need it sorted by the number of pieces.
		SortedMap<Integer, List<Integer>> rarestMap = new TreeMap<Integer, List<Integer>>();
		for(int i=1; i <= fileNumPiecesCompleted.size(); i++) {
			if(peerStatus.get(i) == LEECHER)
				if(!rarestMap.containsKey(fileNumPiecesCompleted.get(i)))
					rarestMap.put(fileNumPiecesCompleted.get(i), new ArrayList<Integer>());
		}
		for(int i=1; i <= fileNumPiecesCompleted.size(); i++) {
			if(peerStatus.get(i) == LEECHER)
				rarestMap.get(fileNumPiecesCompleted.get(i)).add(i);
		}
		
		List<Integer> rarest = new ArrayList<Integer>((size < fileNumPiecesCompleted.size()) ? fileNumPiecesCompleted.size() : size);
		for(Integer nPieces : rarestMap.keySet()) {
			for(Integer i : rarestMap.get(nPieces)) {
				rarest.add(i);
				if(rarest.size() == size)
					return rarest;
			}
		}
		
		return rarest;
	}
	
	/**
	 * Gets the rarest (least completed) piece of a file.
	 * @param file The file.
	 * @return The number of the rarest piece.
	 */
	public int getRarestPieceOfFile(int file) {
		int rarest = -1;
		int blocksCount = Integer.MAX_VALUE;
		
		for(int i=0; i < nPieces; i++) {
			if(getFilePieceBlocksStatus(file, i) < blocksCount) {
				blocksCount =  getFilePieceBlocksStatus(file, i);
				rarest = i;
			}
		}
		
		return rarest;
	}
	
	/**
	 * Fetches a set of peers already known by this peer to have at least one piece of a given file.
	 * @param file The file for which the fetch will be performed.
	 * @param piece The piece of the file for which the fetch will be performed.
	 * @param pid PushPeer's protocol id.
	 * @return A set of peers that have at least one piece of the given file.
	 */
	public Set<Node> getSwarmForFilePiece(int file, int piece, int pid) {
		if(swarm.get(file) == null)
			swarm.put(file, new HashSet<Node>());
		
		//If swarm is too small, fetch from DHT
		if(swarm.size() < swarm_min) {
			swarm.get(file).addAll(SimpleDHT.findNodesByFilePieceBlock(file, piece, 0, (short)(swarm_max - swarm.size())));
			for(Node n : swarm.get(file))
				addKnownPeer(n);
		}
		
		//TODO_L: Optimize this procedure to avoid having to check which pieces a peer has every time the swarm is retrieved.
		Set<Node> returnSet = new HashSet<Node>(swarm.get(file));
		for(Node n : swarm.get(file)) {
			PushPeer peer = (PushPeer)n.getProtocol(pid);
			if(peer.getFilePieceBlocksStatus(file, piece) < NUM_BLOCKS) {
				returnSet.remove(n);
			}
		}		
		if(returnSet.isEmpty()) {
			//TODO_L: Create a backlog swarm to store the useless (for now) peers instead of throwing them away, or replace only a fraction of them.
			swarm.get(file).clear();
			swarm.get(file).addAll(SimpleDHT.findNodesByFilePieceBlock(file, piece, 0, (short)swarm_max));
			returnSet = new HashSet<Node>(swarm.get(file));
			
			for(Node n : swarm.get(file)) {
				PushPeer peer = (PushPeer)n.getProtocol(pid);
				if(peer.getFilePieceBlocksStatus(file, piece) < NUM_BLOCKS) {
					returnSet.remove(n);
				}
			}
		}
		
		return returnSet;
	}
	
	/**
	 * Creates a request event for the next wanted piece of a file.
	 * @param node Reference to the local node which will make the request.
	 * @param pid PushPeer's protocol id.
	 * @param file The file to request.
	 */
	public void nextRequest(Node node, int pid, int file) {
		int myRarestPiece = getRarestPieceOfFile(file);
		
		if(getFilePieceBlocksStatus(file, myRarestPiece) < NUM_BLOCKS) {
			Set<Node> swarm = getSwarmForFilePiece(file, myRarestPiece, pid);
			if(swarm.isEmpty())
				return;
			
			//Sends a request for the local rarest piece to a random node from the swarm.
			//TODO_L: Burst-request many pieces.
			List<Node> swarmAsList = new ArrayList<Node>(swarm);
			Node n = swarmAsList.get(CommonState.r.nextInt(swarmAsList.size()));
			long latency;
			latency = ((Transport)node.getProtocol(tid)).getLatency(node, n);
			Object ev;
			ev = new IntegerEvent(EventType.REQUEST, node, latency, file, myRarestPiece);
			EDSimulator.add(latency, ev, n, pid);
		}
		else {
			//This peer has completed every file!
		}
		
	}
	
	/**
	 * Resets the CB of this peer. Used when it's CB leaves the network.
	 * @param node Reference to the local node which will make the request.
	 * @param pid PushPeer's protocol id.
	 */
	public void resetCB(Node node, int pid) {
		//Modulo Network.size() is used below to even out the probability distribution of the nodes being part of the result.
		setCB(SimpleDHT.findNodeByID(MD5.valueMD5(getThisNodeID())%Network.size()));
		
		long latency = ((Transport)node.getProtocol(tid)).getLatency(node, cb);
		
		Object ev;
		ev = new Event(EventType.REGISTER_IN_CB, node, latency);
		EDSimulator.add(latency, ev, cb, pid);
		
	}
	
	public long getInitialTime() {
		return initialTime;
	}

	public void setInitialTime(long initialTime) {
		this.initialTime = initialTime;
	}
	
	/**
	 * Responsible for the replication strategy.
	 * @param pid PushPeer's protocol id.
	 * @return the file to be requested for cache.
	 */
	protected abstract int replicationStrategy(int pid);
	
	/**
	 * Clone method of the class. Returns a deep copy of the PushPeer class. Used
	 * by the simulation to initialize the {@link peersim.core.Network}.
	 * @return the deep copy of the PushPeer class.
	 */
	public Object clone() {
		Object clone = null;
		try {
			clone = super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		PushPeer clonePP = (PushPeer)clone;
		clonePP.blocksStatus = new HashMap<Integer, List<Integer>>();
		for(Integer i : this.blocksStatus.keySet()) {
			clonePP.blocksStatus.put(i, new ArrayList<Integer>(this.blocksStatus.get(i)));
		}
		clonePP.fileNumPiecesCompleted = new HashMap<Integer, Integer>(this.fileNumPiecesCompleted);
		clonePP.peerStatus = new HashMap<Integer, Integer>(this.peerStatus);
		clonePP.wantedFiles = new HashMap<Integer, Boolean>(this.wantedFiles);
		
		clonePP.managedPeers = new HashSet<Node>(this.managedPeers);
		clonePP.managedPeerCacheCount = new HashMap<Node, Integer>(this.managedPeerCacheCount);
		clonePP.swarm = new HashMap<Integer, Set<Node>>();
		for(Integer i : this.swarm.keySet()) {
			clonePP.swarm.put(i, new HashSet<Node>(this.swarm.get(i)));
		}
		clonePP.knownPeers = new HashSet<Node>(this.knownPeers);
		clonePP.knownPeersQueue = new LinkedList<Node>(this.knownPeersQueue);
		
		clonePP.alreadyPolled = new HashMap<Integer, Boolean>(this.alreadyPolled);
		clonePP.downloadCompletionTime = new HashMap<Integer, Long>(this.downloadCompletionTime);
		
		return clone;
	}
	
	/*******************************************************/
	/**------------------ EVENT HANDLER ------------------**/
	/*******************************************************/
	
	/**
	 * The standard method that processes incoming events. For event types reference see {@link EventType}.
	 * @param node Reference to the local node for which the event is going to be processed.
	 * @param pid PushPeer's protocol id.
	 * @param event The event to process.
	 */
	@Override
	public void processEvent(Node node, int pid, Object event) {
		
		long latency;
		Object evReply;
		
		Event ev = (Event)event;
		switch(ev.getType()) {
		case ASSERT_CACHE:
			//TODO_M: ASSERT_CACHE
			break;
		case CONFIRM:
			//TODO_M: CONFIRM
			break;
		case FETCH_REP:
			/*int value = ((IntegerEvent)ev).getValues()[0];
			PushPeer manPeer = (PushPeer)managedPeers.getProtocol(pid);
			int valueToSend = Integer.MIN_VALUE;
			if(value == manPeer.getThisNodeID())
				valueToSend = managedPeerCacheCount;
			
			evReply = new IntegerEvent(EventType.PEER_REP, node, valueToSend);
			
			latency = ((Transport)node.getProtocol(tid)).getLatency(node, cb);
			
			EDSimulator.add(latency, evReply, cb, pid);
			*/
			break;
		case PEER_REP:
			//TODO_M: PEER_REP
			break;
		case NOTIFY_COMPLETE:
			//TODO_M: NOTIFY_COMPLETE
			/*
			 * Remember to increase managedPeerCacheCount!
			 */
			break;
		case REGISTER_IN_CB:
			this.setManagedPeer(ev.getSender());
			break;
		case REQUEST:
			int requestedFile = ((IntegerEvent)ev).getValues()[0];
			int requestedPiece = ((IntegerEvent)ev).getValues()[1];
			
			//Ignore request if the peer doesn't have the requested file/piece. Shouldn't happen.
			if(getFilePieceBlocksStatus(requestedFile, requestedPiece) != NUM_BLOCKS)
				return;
			
			latency = ((Transport)node.getProtocol(tid)).getLatency(node, cb);
			
			int myBandwidth = getMaxBandwidth();
			int remoteBandwidth = ((PushPeer)node.getProtocol(pid)).getMaxBandwidth();
			
			long uploadTime_partial = (long)(latency + Math.ceil(SIZE_PIECE/Math.min(myBandwidth, remoteBandwidth)));
			//Need to consider the other uploads currently being sent.
			long uploadTime = remainingUploadsTime + uploadTime_partial;
			
			//TODO_M: Use a queue for more precision.
			evReply = new IntegerEvent(EventType.PIECE, node, uploadTime, requestedFile, requestedPiece);
			
			EDSimulator.add(uploadTime, evReply, ev.getSender(), pid);
			
			remainingUploadsTime+=uploadTime_partial;
			
			//Self notification that the upload is finished. Used to decrease the remainingUploadsTime counter.
			EDSimulator.add(uploadTime, new Event(EventType.UPLOAD_FINISHED, node, uploadTime_partial), node, pid);
			
			break;
		case REQUEST_CACHE:
			int fileToCache = ((IntegerEvent)ev).getValues()[0];
			setToBeCachedFile(node, pid, fileToCache);
			break;
		case PIECE:
			int receivedFile = ((IntegerEvent)ev).getValues()[0];
			int receivedPiece = ((IntegerEvent)ev).getValues()[1];

			//Ignore a piece that is not wanted anymore.
			if(getFileWantedStatus(receivedFile) != WANTED)
				return;
			
			setFilePieceBlocksStatus(receivedFile, receivedPiece, NUM_BLOCKS);
			if(getFileNumPiecesCompleted(receivedFile) >= getNPieces()) {
				setFileWantedStatus(receivedFile, !WANTED);
				setPeerStatus(receivedFile, PushPeer.SEEDER);
				downloadCompletionTime.put(receivedFile, CommonState.getTime() - initialTime);
			}
			else {
				nextRequest(node, pid, receivedFile);
			}
			
			//TODO_M: Check if a to be cached file is complete, take necessary actions.
			
			break;
		case UPLOAD_FINISHED:
			remainingUploadsTime-=ev.getLatency();
			break;
		default:
			break;
		}
		
	}

	/*******************************************************/
	/**------------------ CYCLE HANDLER ------------------**/
	/*******************************************************/
	
	/**
	 * Periodic REQUEST_CACHE events will be created here, by a CB to it's managed peer.
	 * @param node Reference to the local node for which the event is going to be processed.
	 * @param pid PushPeer's protocol id.
	 */
	@Override
	public void nextCycle(Node node, int pid) {
		
		//TODO_M: Quickfix for timeouts, caused by peers leaving before the REQUEST event reaches them. Improve this fix.
		for(int i=1; i <= fileNumPiecesCompleted.size(); i++) {
			nextRequest(node, pid, i);
		}
		
		if(!ENABLE_CACHE || managedPeers.isEmpty()) {
			return;
		}
			
		int rarestFile = replicationStrategy(pid);
		
		//TODO_L: Is this an acceptable case? How to properly deal with it?
		if(rarestFile < 0) {
			return;
		}
		
		for(Node n : managedPeers) {
			long latency;
			latency = ((Transport)node.getProtocol(tid)).getLatency(node, cb);
			
			//Second, send the REQUEST_CACHE to the managed peer with the rarest file.
			Object ev;
			ev = new IntegerEvent(EventType.REQUEST_CACHE, node, latency, rarestFile);
			EDSimulator.add(latency, ev, n, pid);
		}
		
	}
}
